#!/usr/bin/python
# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

"""
Console mode implementation.
See :mod:`pylot.interfaces` for API documentation.

"""

__all__ = [
           u'ProcessHandler',
           u'Lock',
           u'DEFAULT_PROCESS_HANDLER',
           ]

import subprocess
import sys
import threading

from traits.api import implements

from _pylot.minimal.commands import *
from _pylot.minimal.utils import (uprint, error)

from _pylot.minimal.default.impl_processhandler import CommonProcessHandler

from _pylot.minimal.interfaces.i_processhandler import ILock
from _pylot.minimal.default.impl_commonrun import CommonRun

class Lock (object):
  implements(ILock)
  def __init__ (self):
    super(Lock, self).__init__()
    self.lock = threading.Lock()

  def acquire (self):
    self.lock.acquire()

  def release (self):
    self.lock.release()

class PythonRun (CommonRun):
  u"""
  Python "run object" using subprocess.Popen object
  """
  def start(self, python_method, args=None, kargs=None):
    if args is None :
      args = []
    if kargs is None :
      kargs = {}
    self._state = self.Running
    try :
      python_method(*args, **kargs)
    except :
      self._return_code = 1
    else :
      self._return_code = 0
    self._state = self.Finished
    return self._return_code

  def _getState (self):
    return self._state

  def _getReturnCode (self):
    return self._return_code

class ShellRun (CommonRun):

  def _getState (self):
    if self.thread.returncode is None :
      return self.Running
    else :
      return self.Finished

  def _getReturnCode (self):
    return self.thread.returncode

  def _kill (self, signal):
    self.thread.kill()

  def _terminate(self):
    self.thread.terminate()


class ProcessHandler (CommonProcessHandler):
  u"""
  >>> from pyLot.core import command_group_sample, ProcessHandler
  >>> handler = ProcessHandler()
  >>> cg = command_group_sample()
  >>> cg = handler.run(cg) #doctest: +SKIP
  ... Start "Command Group 1" ...
  py1: wake up
  >>> cg = handler.runCmd(u'pwd') #doctest: +SKIP
  """

  def __init__(self, *args, **kargs) :
    super(ProcessHandler, self).__init__(*args, **kargs)
    self.locks = {}
    self.lock_start_run = threading.Lock()
    self.modifying_locks = threading.Lock()

  @classmethod
  def runTextCommand (cls, cmd):
    if cmd.style == u'title' :
      uprint(u'=' * len(cmd.text))
    uprint(cmd.text)
    if cmd.style == u'title' :
      uprint(u'=' * len(cmd.text))

  def _waitCG (self, current_thread):
    cg = current_thread
    if cg._wait_list :
      for cg_to_wait in cg._wait_list :
        if cg_to_wait.state == CommonRun.NotRunning :
          # block this thread
          lock = threading.Lock()
          self.modifying_locks.acquire()
          if cg_to_wait in self.locks :
            self.locks[cg_to_wait].append(lock)
          else :
            self.locks[cg_to_wait] = []
            self.locks[cg_to_wait].append(lock)
          self.modifying_locks.release()
          lock.acquire()
          lock.acquire() # freeze this thread until an other thread free it
        elif cg_to_wait.state in (CommonRun.Running, CommonRun.Paused) :
          # block this thread
          cg_to_wait.thread.join()
        elif cg_to_wait.state == CommonRun.Finished :
          continue
        else :
          raise NotImplementedError, error(cg_to_wait.state)
    else :
      # DO NOT BLOCK
      return True

  def _realRun (self, command_group):
    self._waitCG(command_group)
    # all required group finished (success or fail)

    return_code = 0
    for cg_to_wait in command_group._wait_list :
      if cg_to_wait.return_code != 0 :
        return_code = cg_to_wait.return_code
        break

    if return_code == 0 :
      # run it
      self._realRun2(command_group)
    else :
      # force this group to fail
      uprint(tr_(u'Don\'t start "%s" because required CommandGroup'\
        u' "%s" fails (rcode=%s)') % (
          command_group.alias, cg_to_wait.alias, cg_to_wait.return_code),
          file=sys.stderr)
      command_group.cmds[-1].STATE = CommonRun.Finished
      command_group.cmds[-1].RETURN_CODE = return_code

    # cg is finished, we can release all locks
    self.modifying_locks.acquire()
    if command_group in self.locks :
      length = len(self.locks[command_group])
      for i in range(length) :
        self.locks[command_group].pop().release()
    self.modifying_locks.release()

  def _realRun2 (self, command_group):
    cg = command_group

    def write(text, style=None):
      self.logger.write(text, style)
      cg.logger.write(text, style)
    def write_stdout(s):
      self.logger.stdout(s)
      cg.logger.stdout(s)
    def write_stderr(s):
      self.logger.write(text, style)
      cg.logger.stderr(s)

    text, style, level = self.formatTextCmdGroupStarted(command_group)
    write(text, style)

    cg.exec_time.start()

    for i, cmd in enumerate(cg.cmds) :
      #a, b = cg.progress
      #percent = float(a + 1) / b * 100
      #self.io.write('[' + str(percent) + '% ]', level='verbose')

      text, style, level = self.formatTextCmdStarted(cmd)

      cmd.exec_time.start()
      # ManualCommand
      if type(cmd) in (ManualCommand, SpecialCommand) :
        # Manual Command finishes successfully
        cmd.STATE = CommonRun.Finished
        if cmd.RETURN_CODE is None :
          cmd.RETURN_CODE = 0

      # TextCommand, PythonCommand
      elif type(cmd) in (TextCommand, PythonCommand) :
        run_obj = PythonRun(None)
        cmd.thread = run_obj
        if type(cmd) is TextCommand :
          run_obj.start(self.runTextCommand, [cmd])
          cmd.STATE = run_obj.state
          cmd.RETURN_CODE = run_obj.return_code
        else :
          run_obj.start(cmd.python_method, cmd.args, cmd.kargs)
#          cmd.python_method(*cmd.args, **cmd.kargs)
      # ShellCommand
      elif type(cmd) is ShellCommand :
        # stdout and stderr ared defined before command loop (in this method)
        cwd = cmd.working_directory
        if not cwd.isdir() :
          cwd.mkdir()
        popen_obj = subprocess.Popen(cmd.shellUString(), shell=True,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE, cwd=cwd)
        cmd.thread = ShellRun(popen_obj)
        stdout, stderr = popen_obj.communicate()
        write_stderr(stderr)
        cmd.logger.stderr(stderr)
        write_stdout(stdout)
        cmd.logger.stdout(stdout)
      else :
        write(u'%s not implemented' % cmd)

      cmd.exec_time.stop()
      text, style, level = self.formatTextCmdExecTime(cmd)
      write(text, style)

      if cmd.return_code != 0 :
        break

      cg._progress[0] += 1

    cg.exec_time.stop()
    text, style, level = self.formatTextCmdGroupFinished(command_group)
    write(text, style)
    text, style, level = self.formatTextCmdGroupExecTime(command_group)
    write(text, style)

    return cg.return_code

  def _run (self, command_group, wait):
    self.lock_start_run.acquire()

    if command_group.state == CommonRun.Running :
      txt = tr_(u'I cannot start command group because group is Running')
      uprint(txt, file=sys.stderr)
    elif command_group.state == CommonRun.Finished :
      txt = tr_(u'I cannot start command group because group has been launched and is finished')
      uprint(txt, file=sys.stderr)
    else :
#      command_group.cmds[0].STATE = ICommonRun.Running
      thread = threading.Thread(target=self._realRun,
                                args=[command_group])
      thread.start()
      command_group.thread = thread

      if wait is True :
        thread.join() # Ok
#        self._realRun(command_group)
#        self.requireGroups(command_group) # Must be debugged !!!

    self.lock_start_run.release()
    return command_group

  def _requireGroups(self, *args):
    for command_group in args :
      if command_group not in self._wait_list :
        self._wait_list.append(command_group)

    self._waitCG(command_group)
    # all required group finished (success or fail)

    self.modifying_locks.acquire()
    return_code = 0
    for cg_to_wait in self._wait_list :
      if cg_to_wait.return_code != 0 :
        return_code = cg_to_wait.return_code
        break

    # empty list
    length = len(self._wait_list)
    for i in range(length) :
      self._wait_list.pop()

    if return_code == 0 :
      # continue execution
      pass
    else :
      # signal a "required process" fails !
      try :
        group_alias = u'"%s"' % cg_to_wait.alias
      except NameError :
        group_alias = u'"?"'
      txt = u'required CommandGroup %s fails with return code %s' % (group_alias, return_code)
#      self.io.write(txt,
#        writers=self.io.textout_writers + cg_to_wait.textout_writers)
    self.modifying_locks.release()

DEFAULT_PROCESS_HANDLER = ProcessHandler()
#DEFAULT_IO_HANDLER = DEFAULT_PROCESS_HANDLER.io

if __name__ == u'__main__':

  import doctest
  doctest.testmod()


